using System.Threading.Tasks;

namespace System.IO.Abstractions.TestingHelpers.Tests;

using Collections.Generic;

using NUnit.Framework;

using XFS = MockUnixSupport;

public class MockFileOpenTests
{
    [Test]
    public async Task MockFile_Open_ThrowsOnCreateNewWithExistingFile()
    {
        string filepath = XFS.Path(@"c:\something\already\exists.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
        {
            { filepath, new MockFileData("I'm here") }
        });

        await That(() => filesystem.File.Open(filepath, FileMode.CreateNew)).Throws<IOException>();
    }

    [Test]
    public async Task MockFile_Open_ThrowsOnOpenWithMissingFile()
    {
        string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());

        await That(() => filesystem.File.Open(filepath, FileMode.Open)).Throws<FileNotFoundException>();
    }

    [Test]
    public async Task MockFile_Open_ThrowsOnTruncateWithMissingFile()
    {
        string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());

        await That(() => filesystem.File.Open(filepath, FileMode.Truncate)).Throws<FileNotFoundException>();
    }

    [Test]
    public async Task MockFile_Open_CreatesNewFileFileOnCreate()
    {
        string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());
        filesystem.AddDirectory(XFS.Path(@"c:\something\doesnt"));

        var stream = filesystem.File.Open(filepath, FileMode.Create);

        await That(filesystem.File.Exists(filepath)).IsTrue();
        await That(stream.Position).IsEqualTo(0);
        await That(stream.Length).IsEqualTo(0);
    }

    [Test]
    public async Task MockFile_Open_AllowsReadWriteOnCreate()
    {
        string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());
        filesystem.AddDirectory(XFS.Path(@"c:\something\doesnt"));

        var stream = filesystem.File.Open(filepath, FileMode.Create);

        await That(stream.CanRead).IsTrue();
        await That(stream.CanWrite).IsTrue();
    }

    [Test]
    public async Task MockFile_Open_CreatesNewFileFileOnCreateNew()
    {
        string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());
        filesystem.AddDirectory(XFS.Path(@"c:\something\doesnt"));

        var stream = filesystem.File.Open(filepath, FileMode.CreateNew);

        await That(filesystem.File.Exists(filepath)).IsTrue();
        await That(stream.Position).IsEqualTo(0);
        await That(stream.Length).IsEqualTo(0);
    }

    [Test]
    public async Task MockFile_Open_OpensExistingFileOnAppend()
    {
        string filepath = XFS.Path(@"c:\something\does\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
        {
            { filepath, new MockFileData("I'm here") }
        });

        var stream = filesystem.File.Open(filepath, FileMode.Append);
        var file = filesystem.GetFile(filepath);

        await That(stream.Position).IsEqualTo(file.Contents.Length);
        await That(stream.Length).IsEqualTo(file.Contents.Length);
    }

    [Test]
    public async Task MockFile_Open_OpensExistingFileOnTruncate()
    {
        string filepath = XFS.Path(@"c:\something\does\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
        {
            { filepath, new MockFileData("I'm here") }
        });

        var stream = filesystem.File.Open(filepath, FileMode.Truncate);
        var file = filesystem.GetFile(filepath);

        await That(stream.Position).IsEqualTo(0);
        await That(stream.Length).IsEqualTo(0);
        await That(file.Contents.Length).IsEqualTo(0);
    }

    [Test]
    public async Task MockFile_Open_OpensExistingFileOnOpen()
    {
        string filepath = XFS.Path(@"c:\something\does\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
        {
            { filepath, new MockFileData("I'm here") }
        });

        var stream = filesystem.File.Open(filepath, FileMode.Open);
        var file = filesystem.GetFile(filepath);

        await That(stream.Position).IsEqualTo(0);
        await That(stream.Length).IsEqualTo(file.Contents.Length);

        byte[] data;
        using (var br = new BinaryReader(stream))
            data = br.ReadBytes((int)stream.Length);

        await That(data).IsEqualTo(file.Contents);
    }

    [Test]
    public async Task MockFile_Open_OpensExistingFileOnOpenOrCreate()
    {
        string filepath = XFS.Path(@"c:\something\does\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
        {
            { filepath, new MockFileData("I'm here") }
        });

        var stream = filesystem.File.Open(filepath, FileMode.OpenOrCreate);
        var file = filesystem.GetFile(filepath);

        await That(stream.Position).IsEqualTo(0);
        await That(stream.Length).IsEqualTo(file.Contents.Length);

        byte[] data;
        using (var br = new BinaryReader(stream))
            data = br.ReadBytes((int)stream.Length);

        await That(data).IsEqualTo(file.Contents);
    }

    [Test]
    public async Task MockFile_Open_CreatesNewFileOnOpenOrCreate()
    {
        string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());
        filesystem.AddDirectory(XFS.Path(@"c:\something\doesnt"));

        var stream = filesystem.File.Open(filepath, FileMode.OpenOrCreate);

        await That(filesystem.File.Exists(filepath)).IsTrue();
        await That(stream.Position).IsEqualTo(0);
        await That(stream.Length).IsEqualTo(0);
    }

    [Test]
    public async Task MockFile_Open_OverwritesExistingFileOnCreate()
    {
        string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
        var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
        {
            { filepath, new MockFileData("I'm here") }
        });

        var stream = filesystem.File.Open(filepath, FileMode.Create);
        var file = filesystem.GetFile(filepath);

        await That(stream.Position).IsEqualTo(0);
        await That(stream.Length).IsEqualTo(0);
        await That(file.Contents.Length).IsEqualTo(0);
    }

    [Test]
    public async Task MockFile_OpenText_ShouldRetainLastWriteTime()
    {
        // Arrange
        var fs = new MockFileSystem();
        string filepath = XFS.Path(@"C:\TestData\test.txt");
        var file = new MockFileData(@"I'm here");
        var lastWriteTime = new DateTime(2012, 03, 21);
        file.LastWriteTime = lastWriteTime;
        fs.AddFile(filepath, file);

        // Act
        using (var reader = fs.File.OpenText(filepath))
        {
            reader.ReadLine();
        }

        // Assert
        await That(fs.FileInfo.New(filepath).LastWriteTime).IsEqualTo(lastWriteTime);
    }

    [Test]
    public async Task MockFile_OpenText_ShouldUpdateLastAccessTime()
    {
        // Arrange
        var fs = new MockFileSystem();
        string filepath = XFS.Path(@"C:\TestData\test.txt");
        var file = new MockFileData(@"I'm here");
        var lastAccessTime = new DateTime(2012, 03, 21);
        file.LastAccessTime = lastAccessTime;
        fs.AddFile(filepath, file);

        // Act
        using (var reader = fs.File.OpenText(filepath))
        {
            reader.ReadLine();
        }

        // Assert
        await That(DateTime.Now - fs.FileInfo.New(filepath).LastAccessTime).IsLessThanOrEqualTo(TimeSpan.FromSeconds(1));
    }

    [Test]
    public async Task MockFile_Read_ShouldRetainCreationTimeAndUpdateLastAccessTime()
    {
        // Arrange
        var fs = new MockFileSystem();
        var filepath = XFS.Path(@"C:\TestData\test.txt");
        var file = new MockFileData(new byte[] { 1, 2, 3 });
        var lastAccessTime = new DateTime(2012, 03, 21);
        file.LastAccessTime = lastAccessTime;
        var creationTime = new DateTime(2012, 03, 20);
        file.CreationTime = creationTime;
        fs.AddFile(filepath, file);

        var fi = fs.FileInfo.New(filepath);
        var stream = fi.OpenRead();
        var buffer = new byte[16];
#pragma warning disable CA2022
        stream.Read(buffer, 0, buffer.Length);
#pragma warning restore CA2022
        fi.Refresh();
        // Assert
        await That(fi.CreationTime).IsEqualTo(creationTime);
        await That(DateTime.Now - fi.LastAccessTime).IsLessThanOrEqualTo(TimeSpan.FromSeconds(1));
    }

    [Test]
    public async Task MockFile_ReadAsync_ShouldRetainCreationTimeAndUpdateLastAccessTime()
    {
        // Arrange
        var fs = new MockFileSystem();
        var filepath = XFS.Path(@"C:\TestData\test.txt");
        var file = new MockFileData(new byte[] { 1, 2, 3 });
        var lastAccessTime = new DateTime(2012, 03, 21);
        file.LastAccessTime = lastAccessTime;
        var creationTime = new DateTime(2012, 03, 20);
        file.CreationTime = creationTime;
        fs.AddFile(filepath, file);

        var fi = fs.FileInfo.New(filepath);
        var stream = fi.OpenRead();
        var buffer = new byte[16];
#pragma warning disable CA1835
#pragma warning disable CA2022
        // ReSharper disable once MustUseReturnValue
        await stream.ReadAsync(buffer, 0, buffer.Length);
#pragma warning restore CA2022
#pragma warning restore CA1835
        fi.Refresh();
        // Assert
        await That(fi.CreationTime).IsEqualTo(creationTime);
        await That(DateTime.Now - fi.LastAccessTime).IsLessThanOrEqualTo(TimeSpan.FromSeconds(1));
    }

    [Test]
    public async Task MockFile_Write_ShouldRetainCreationTimeAndUpdateLastAccessTimeAndLastWriteTime()
    {
        // Arrange
        var fs = new MockFileSystem();
        var filepath = XFS.Path(@"C:\TestData\test.txt");
        var file = new MockFileData(new byte[] { 1, 2, 3 });
        var lastAccessTime = new DateTime(2012, 03, 21);
        file.LastAccessTime = lastAccessTime;
        var creationTime = new DateTime(2012, 03, 20);
        file.CreationTime = creationTime;
        fs.AddFile(filepath, file);

        var fi = fs.FileInfo.New(filepath);
        var stream = fi.OpenWrite();
        var buffer = new byte[16];
        stream.Write(buffer, 0, buffer.Length);
        fi.Refresh();
        // Assert
        await That(fi.CreationTime).IsEqualTo(creationTime);
        await That(DateTime.Now - fi.LastAccessTime).IsLessThanOrEqualTo(TimeSpan.FromSeconds(1));
        await That(DateTime.Now - fi.LastWriteTime).IsLessThanOrEqualTo(TimeSpan.FromSeconds(1));
    }

    [Test]
    public async Task MockFile_WriteAsync_ShouldRetainCreationTimeAndUpdateLastAccessTimeAndLastWriteTime()
    {
        // Arrange
        var fs = new MockFileSystem();
        var filepath = XFS.Path(@"C:\TestData\test.txt");
        var file = new MockFileData(new byte[] { 1, 2, 3 });
        var lastAccessTime = new DateTime(2012, 03, 21);
        file.LastAccessTime = lastAccessTime;
        var creationTime = new DateTime(2012, 03, 20);
        file.CreationTime = creationTime;
        fs.AddFile(filepath, file);

        var fi = fs.FileInfo.New(filepath);
        var stream = fi.OpenWrite();
        var buffer = new byte[16];
        await stream.WriteAsync(buffer, 0, buffer.Length);
        fi.Refresh();
        // Assert
        await That(fi.CreationTime).IsEqualTo(creationTime);
        await That(DateTime.Now - fi.LastAccessTime).IsLessThanOrEqualTo(TimeSpan.FromSeconds(1));
        await That(DateTime.Now - fi.LastWriteTime).IsLessThanOrEqualTo(TimeSpan.FromSeconds(1));
    }

    [Test]
    public async Task MockFile_OpenText_ShouldRetainCreationTime()
    {
        // Arrange
        var fs = new MockFileSystem();
        string filepath = XFS.Path(@"C:\TestData\test.txt");
        var file = new MockFileData(@"I'm here");
        var creationTime = new DateTime(2012, 03, 21);
        file.CreationTime = creationTime;
        fs.AddFile(filepath, file);

        // Act
        using (var reader = fs.File.OpenText(filepath))
        {
            reader.ReadLine();
        }

        // Assert
        await That(fs.FileInfo.New(filepath).CreationTime).IsEqualTo(creationTime);
    }

    [Test]
    public async Task MockFile_Open_ShouldThrowDirectoryNotFoundExceptionIfFileModeCreateAndParentPathDoesNotExist()
    {
        // Arrange
        var fileSystem = new MockFileSystem();
        var file = XFS.Path("C:\\path\\NotFound.ext");

        // Act
        Action action = () => fileSystem.File.Open(file, FileMode.Create);

        // Assert
        var exception = await That(action).Throws<DirectoryNotFoundException>();
        await That(exception.Message).StartsWith("Could not find a part of the path");
    }

    [Test]
    public async Task MockFile_OpenWrite_ShouldWorkWithRelativePath()
    {
        var file = "file.txt";
        var fileSystem = new MockFileSystem();

        fileSystem.File.OpenWrite(file).Close();

        await That(fileSystem.File.Exists(file)).IsTrue();
    }
}